Add cross-SDK request validator test vectors and fix URL normalization#39
Add cross-SDK request validator test vectors and fix URL normalization#39
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves webhook signature validation by making URL normalization accept inputs without an explicit scheme (by prepending http://) and adds a parameterized test suite of cross-SDK URL normalization vectors to lock in expected signatures.
Changes:
- Prepend
http://when the input URL lacks a://scheme delimiter. - Add 15 URL normalization/signature “golden” vectors via a JUnit 5 parameterized test.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/main/java/com/didww/sdk/callback/RequestValidator.java |
Adjusts URL normalization to handle no-scheme URLs by prepending http://. |
src/test/java/com/didww/sdk/callback/RequestValidatorTest.java |
Adds parameterized test vectors to validate URL normalization + signature results across common URL variants. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!url.matches("^[a-zA-Z]+://.*")) { | ||
| url = "http://" + url; | ||
| } | ||
| URI uri = URI.create(url); | ||
| String scheme = uri.getScheme(); | ||
| String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : ""; |
There was a problem hiding this comment.
normalizeUrl uses uri.getScheme() later to pick default ports and also includes it in the normalized URL. URI preserves the original scheme casing (e.g., HTTPS://... -> scheme "HTTPS"), but schemes are case-insensitive; this can cause signature mismatches and wrong default-port selection. Consider lowercasing the parsed scheme (and using the lowercased value consistently for both port selection and the returned normalized URL).
| if (!url.matches("^[a-zA-Z]+://.*")) { | ||
| url = "http://" + url; | ||
| } |
There was a problem hiding this comment.
String#matches recompiles the regex on every call. Since normalizeUrl runs for every validation, consider avoiding regex here (e.g., a small character scan / indexOf("://")) or using a precompiled Pattern constant. If you keep regex, [A-Za-z][A-Za-z0-9+.-]*:// is a more RFC-compatible scheme pattern than ^[a-zA-Z]+://.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| URI uri = URI.create(url); | ||
| String scheme = uri.getScheme(); | ||
| String scheme = uri.getScheme().toLowerCase(); |
There was a problem hiding this comment.
uri.getScheme().toLowerCase() is locale-sensitive and can produce incorrect results under certain default locales (e.g., Turkish), which would break signature validation. Use locale-independent lowercasing (e.g., toLowerCase(Locale.ROOT)) when normalizing the scheme.
| static Stream<Object[]> urlNormalizationVectors() { | ||
| return Stream.of( | ||
| new Object[]{"http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, | ||
| new Object[]{"http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, |
There was a problem hiding this comment.
The method source currently returns Stream<Object[]>, which is less type-safe and harder to read/maintain than using JUnit 5's Stream<Arguments> (or a dedicated record/class). Switching to Arguments.of(url, expectedSignature) will improve clarity and avoids accidental argument-shape mistakes.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| RequestValidator validator = new RequestValidator("SOMEAPIKEY"); | ||
| Map<String, String> payload = new LinkedHashMap<>(); | ||
| payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); | ||
| payload.put("status", "completed"); | ||
| payload.put("type", "orders"); | ||
|
|
There was a problem hiding this comment.
The payload setup here is duplicated across multiple tests in this class (same keys/values with only ordering differences). Extracting a small helper (e.g., a method that returns the common payload map) would reduce repetition and make it easier to update the test data consistently if the signature inputs change again.
| if (!SCHEME_PATTERN.matcher(url).find()) { | ||
| url = "http://" + url; | ||
| } | ||
| URI uri = URI.create(url); | ||
| String scheme = uri.getScheme(); | ||
| String scheme = uri.getScheme().toLowerCase(Locale.ROOT); | ||
| String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : ""; | ||
| String host = uri.getHost(); |
There was a problem hiding this comment.
normalizeUrl prepends http:// when no scheme is detected, but some inputs (e.g., /path, //foo.com/bar, or other non-host relative forms) will parse with uri.getHost() == null and the method will return a normalized URL containing the literal string null as the host. Consider explicitly validating that uri.getHost() is non-null/non-empty (and optionally handling scheme-relative URLs starting with //) and returning ""/failing validation instead of producing a canonical form with a null host.
a7e30cc to
f3c89f0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static Stream<Arguments> urlNormalizationVectors() { | ||
| return Stream.of( | ||
| Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), | ||
| Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), | ||
| Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"), | ||
| Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"), | ||
| Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"), | ||
| Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"), | ||
| Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"), | ||
| Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | ||
| Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | ||
| Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"), | ||
| Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"), | ||
| Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"), | ||
| Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"), | ||
| Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508") | ||
| ); |
There was a problem hiding this comment.
PR description mentions 'Added 15 hardcoded cross-SDK test vectors', but this method currently defines 14 Arguments.of(...) entries. Either add the missing vector or adjust the PR description so it matches the code.
| static Stream<Arguments> urlNormalizationVectors() { | ||
| return Stream.of( | ||
| Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), | ||
| Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), | ||
| Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"), | ||
| Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"), | ||
| Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"), | ||
| Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"), | ||
| Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"), | ||
| Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | ||
| Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | ||
| Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"), | ||
| Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"), | ||
| Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"), | ||
| Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"), | ||
| Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508") |
There was a problem hiding this comment.
Given the PR claims a fix for no-schema URL handling (prepending http://), the normalization vectors should include at least one no-scheme case (e.g., foo.com/bar) to lock in that behavior. Without it, the reported bug fix isn't covered by tests.
| sb.append(String.format("%02x", b)); // NOSONAR | ||
| } | ||
| return sb.toString(); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to compute HMAC-SHA1", e); | ||
| throw new RuntimeException("Failed to compute HMAC-SHA1", e); // NOSONAR |
There was a problem hiding this comment.
Using // NOSONAR here suppresses all static-analysis findings on the line, including future ones. Prefer addressing the underlying warning (e.g., avoid String.format in the loop) or use a more targeted suppression so real issues in this code path aren't hidden.
| try { | ||
| URI uri = URI.create(url); | ||
| String scheme = uri.getScheme(); | ||
| String scheme = uri.getScheme().toLowerCase(Locale.ROOT); |
There was a problem hiding this comment.
uri.getScheme() can be null for no-scheme inputs like foo.com/bar. Calling .toLowerCase(Locale.ROOT) will throw NPE and normalizeUrl will fall back to returning an empty string, which breaks the stated 'prepend http://' behavior and changes signature computation. Handle a null scheme explicitly (e.g., treat as http and re-parse with http:// prefix) before lowercasing.
| String scheme = uri.getScheme().toLowerCase(Locale.ROOT); | |
| String scheme = uri.getScheme(); | |
| if (scheme == null) { | |
| uri = URI.create("http://" + url); | |
| scheme = uri.getScheme(); | |
| } | |
| scheme = scheme.toLowerCase(Locale.ROOT); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| StringBuilder sb = new StringBuilder(); | ||
| for (byte b : rawHmac) { | ||
| sb.append(String.format("%02x", b)); | ||
| sb.append(String.format("%02x", b)); // NOSONAR |
There was a problem hiding this comment.
Avoid using // NOSONAR to suppress the locale warning here; instead pass an explicit locale to String.format (e.g., Locale.ROOT). This keeps the intent clear and removes the need for suppression while still producing stable lowercase hex output across environments.
| sb.append(String.format("%02x", b)); // NOSONAR | |
| sb.append(String.format(Locale.ROOT, "%02x", b)); |
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to compute HMAC-SHA1", e); | ||
| throw new RuntimeException("Failed to compute HMAC-SHA1", e); // NOSONAR | ||
| } |
There was a problem hiding this comment.
Similarly, the // NOSONAR suppression on the thrown exception makes it harder to understand what is being suppressed and why. If Sonar is flagging the exception type/message, it’s better to address the underlying finding (e.g., narrowing the caught exceptions or using a more specific exception type) rather than suppressing it inline.
| static Stream<Arguments> urlNormalizationVectors() { | ||
| return Stream.of( | ||
| Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), | ||
| Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), | ||
| Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"), | ||
| Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"), | ||
| Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"), | ||
| Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"), | ||
| Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"), | ||
| Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | ||
| Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | ||
| Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"), | ||
| Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"), | ||
| Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"), | ||
| Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"), | ||
| Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508") | ||
| ); | ||
| } |
There was a problem hiding this comment.
The PR description mentions “15 hardcoded … test vectors”, but urlNormalizationVectors() currently contains 14 entries. Either add the missing vector or update the PR description so it matches what’s being tested.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"), | ||
| Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"), | ||
| Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"), | ||
| Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), |
There was a problem hiding this comment.
The URL normalization change lowercases the scheme, but the new test vectors don't exercise mixed/upper-case schemes (e.g., "HTTP://foo.com/bar"). Adding a vector that differs only by scheme case would ensure the behavior stays consistent and guards against regressions in normalizeUrl(...).
| Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | |
| Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | |
| Arguments.of("HTTPS://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), |
…alidator - Lowercase the URI scheme after parsing to handle mixed-case schemes correctly - Replace url.matches() with a precompiled static Pattern to avoid recompiling the regex on every call
- Use toLowerCase(Locale.ROOT) for scheme normalization to avoid locale-dependent behavior (e.g. Turkish locale) - Restore SCHEME_PATTERN and no-scheme URL handling - Change Stream<Object[]> to Stream<Arguments> with Arguments.of() for type-safe JUnit 5 parameterized tests - Restore URL normalization test vectors
- Remove SCHEME_PATTERN constant and the no-schema URL prepend logic from RequestValidator; http/https URLs are always expected
- Remove the schema-less test vector ("foo.com/bar") from urlNormalizationVectors
- Add NOSONAR comments on String.format and RuntimeException catch in hmacSha1
Add IPv6, empty path, percent-encoded path vectors from Go SDK. Remove no-scheme URL support.
2486d6a to
cd93d57
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| byte[] signatureBytes = hexToBytes(signature); | ||
| if (signatureBytes == null) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
To fully benefit from a timing-safe comparison and to keep the validator strict, consider rejecting signatures that decode to a different byte length than the computed HMAC (HmacSHA1 is 20 bytes). As written, MessageDigest.isEqual will early-return on length mismatch, and non-standard-length hex signatures (e.g., 2/4/100 hex chars) will be accepted as valid input formats rather than being treated as invalid upfront.
| } | |
| } | |
| if (signatureBytes.length != expectedBytes.length) { | |
| return false; | |
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/main/java/com/didww/sdk/callback/RequestValidator.java:63
normalizeUrl()rebuilds the authority usinghostdirectly. For IPv6 literals,URI#getHost()returns the address without brackets (e.g.::1), so the normalized URL becomes something likehttp://::1:80/..., which is not a valid URL form and may diverge from other SDKs’ canonicalization. Consider re-wrapping IPv6 hosts in[/]when constructing the normalized URL.
String path = uri.getRawPath();
String query = uri.getRawQuery() != null ? "?" + uri.getRawQuery() : "";
String fragment = uri.getRawFragment() != null ? "#" + uri.getRawFragment() : "";
return scheme + "://" + userInfo + host + ":" + port + path + query + fragment;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|



Summary
MessageDigest.isEqual)Test plan